<?php
/**
 * GAPI - Google Analytics PHP Interface
 * 
 * http://code.google.com/p/gapi-google-analytics-php-interface/
 * 
 * @copyright Stig Manning 2009
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @author Stig Manning <stig@sdm.co.nz>
 * @version 1.3
 */
class Dmf_Gapi
{

    const http_interface = 'curl'; //'auto': autodetect, 'curl' or 'fopen'
    const client_login_url = 'https://www.google.com/accounts/ClientLogin';
    const account_data_url = 'https://www.google.com/analytics/feeds/accounts/default';
    const report_data_url = 'https://www.google.com/analytics/feeds/data';
    const interface_name = 'GAPI-1.3';
    const dev_mode = false;

    private $auth_token = null;
    private $account_entries = array( );
    private $account_root_parameters = array( );
    private $report_aggregate_metrics = array( );
    private $report_root_parameters = array( );
    private $results = array( );

    /**
     * Return the auth token, used for storing the auth token in the user session
     *
     * @return String
     */
    public function getAuthToken()
    {
        return $this->auth_token;
    }

    /**
     * Request account data from Google Analytics
     *
     * @param Int $start_index OPTIONAL: Start index of results
     * @param Int $max_results OPTIONAL: Max results returned
     */
    public function requestAccountData( $start_index = 1, $max_results = 20 )
    {
        $response = $this->httpRequest( Dmf_Gapi::account_data_url, array( 'start-index' => $start_index, 'max-results' => $max_results ), null, $this->generateAuthHeader() );

        if ( substr( $response[ 'code' ], 0, 1 ) == '2' )
        {
            return $this->accountObjectMapper( $response[ 'body' ] );
        }
        else
        {
            throw new Exception( 'GAPI: Failed to request account data. Error: "' . strip_tags( $response[ 'body' ] ) . '"' );
        }
    }

    /**
     * Request report data from Google Analytics
     *
     * $report_id is the Google report ID for the selected account
     * 
     * $parameters should be in key => value format
     * 
     * @param String $report_id
     * @param Array $dimensions Google Analytics dimensions e.g. array('browser')
     * @param Array $metrics Google Analytics metrics e.g. array('pageviews')
     * @param Array $sort_metric OPTIONAL: Dimension or dimensions to sort by e.g.('-visits')
     * @param String $filter OPTIONAL: Filter logic for filtering results
     * @param String $start_date OPTIONAL: Start of reporting period
     * @param String $end_date OPTIONAL: End of reporting period
     * @param Int $start_index OPTIONAL: Start index of results
     * @param Int $max_results OPTIONAL: Max results returned
     */
    public function requestReportData( $report_id, $dimensions, $metrics, $sort_metric = null, $filter = null, $start_date = null, $end_date = null, $start_index = 1, $max_results = 32 )
    {
        $parameters = array( 'ids' => 'ga:' . $report_id );

        if ( is_array( $dimensions ) )
        {
            $dimensions_string = '';
            foreach ( $dimensions as $dimesion )
            {
                $dimensions_string .= ',ga:' . $dimesion;
            }
            $parameters[ 'dimensions' ] = substr( $dimensions_string, 1 );
        }
        else
        {
            $parameters[ 'dimensions' ] = 'ga:' . $dimensions;
        }

        if ( is_array( $metrics ) )
        {
            $metrics_string = '';
            foreach ( $metrics as $metric )
            {
                $metrics_string .= ',ga:' . $metric;
            }
            $parameters[ 'metrics' ] = substr( $metrics_string, 1 );
        }
        else
        {
            $parameters[ 'metrics' ] = 'ga:' . $metrics;
        }

        if ( $sort_metric == null && isset( $parameters[ 'metrics' ] ) )
        {
            $parameters[ 'sort' ] = $parameters[ 'metrics' ];
        }
        elseif ( is_array( $sort_metric ) )
        {
            $sort_metric_string = '';

            foreach ( $sort_metric as $sort_metric_value )
            {
                //Reverse sort - Thanks Nick Sullivan
                if ( substr( $sort_metric_value, 0, 1 ) == "-" )
                {
                    $sort_metric_string .= ',-ga:' . substr( $sort_metric_value, 1 ); // Descending
                }
                else
                {
                    $sort_metric_string .= ',ga:' . $sort_metric_value; // Ascending
                }
            }

            $parameters[ 'sort' ] = substr( $sort_metric_string, 1 );
        }
        else
        {
            if ( substr( $sort_metric, 0, 1 ) == "-" )
            {
                $parameters[ 'sort' ] = '-ga:' . substr( $sort_metric, 1 );
            }
            else
            {
                $parameters[ 'sort' ] = 'ga:' . $sort_metric;
            }
        }

        if ( $filter != null )
        {
            $filter = $this->processFilter( $filter );
            if ( $filter !== false )
            {
                $parameters[ 'filters' ] = $filter;
            }
        }

        if ( $start_date == null )
        {
            $start_date = date( 'Y-m-d', strtotime( '1 month ago' ) );
        }

        $parameters[ 'start-date' ] = $start_date;

        if ( $end_date == null )
        {
            $end_date = date( 'Y-m-d' );
        }

        $parameters[ 'end-date' ] = $end_date;


        $parameters[ 'start-index' ] = $start_index;
        $parameters[ 'max-results' ] = $max_results;

        $parameters[ 'prettyprint' ] = Dmf_Gapi::dev_mode ? 'true' : 'false';

        $response = $this->httpRequest( Dmf_Gapi::report_data_url, $parameters, null, $this->generateAuthHeader() );

        //HTTP 2xx
        if ( substr( $response[ 'code' ], 0, 1 ) == '2' )
        {
            return $this->reportObjectMapper( $response[ 'body' ] );
        }
        else
        {
            throw new Exception( 'GAPI: Failed to request report data. Error: "' . strip_tags( $response[ 'body' ] ) . '"' );
        }
    }

    /**
     * Process filter string, clean parameters and convert to Google Analytics
     * compatible format
     * 
     * @param String $filter
     * @return String Compatible filter string
     */
    protected function processFilter( $filter )
    {
        $valid_operators = '(!~|=~|==|!=|>|<|>=|<=|=@|!@)';

        $filter = preg_replace( '/\s\s+/', ' ', trim( $filter ) ); //Clean duplicate whitespace
        $filter = str_replace( array( ',', ';' ), array( '\,', '\;' ), $filter ); //Escape Google Analytics reserved characters
        $filter = preg_replace( '/(&&\s*|\|\|\s*|^)([a-z]+)(\s*' . $valid_operators . ')/i', '$1ga:$2$3', $filter ); //Prefix ga: to metrics and dimensions
        $filter = preg_replace( '/[\'\"]/i', '', $filter ); //Clear invalid quote characters
        $filter = preg_replace( array( '/\s*&&\s*/', '/\s*\|\|\s*/', '/\s*' . $valid_operators . '\s*/' ), array( ';', ',', '$1' ), $filter ); //Clean up operators

        if ( strlen( $filter ) > 0 )
        {
            return urlencode( $filter );
        }
        else
        {
            return false;
        }
    }

    /**
     * Report Account Mapper to convert the XML to array of useful PHP objects
     *
     * @param String $xml_string
     * @return Array of gapiAccountEntry objects
     */
    protected function accountObjectMapper( $xml_string )
    {
        $xml = simplexml_load_string( $xml_string );

        $this->results = null;

        $results = array( );
        $account_root_parameters = array( );

        //Load root parameters

        $account_root_parameters[ 'updated' ] = strval( $xml->updated );
        $account_root_parameters[ 'generator' ] = strval( $xml->generator );
        $account_root_parameters[ 'generatorVersion' ] = strval( $xml->generator->attributes() );

        $open_search_results = $xml->children( 'http://a9.com/-/spec/opensearchrss/1.0/' );

        foreach ( $open_search_results as $key => $open_search_result )
        {
            $report_root_parameters[ $key ] = intval( $open_search_result );
        }

        $account_root_parameters[ 'startDate' ] = strval( $google_results->startDate );
        $account_root_parameters[ 'endDate' ] = strval( $google_results->endDate );

        //Load result entries

        foreach ( $xml->entry as $entry )
        {
            $properties = array( );
            foreach ( $entry->children( 'http://schemas.google.com/analytics/2009' )->property as $property )
            {
                $properties[ str_replace( 'ga:', '', $property->attributes()->name ) ] = strval( $property->attributes()->value );
            }

            $properties[ 'title' ] = strval( $entry->title );
            $properties[ 'updated' ] = strval( $entry->updated );

            $results[ ] = new gapiAccountEntry( $properties );
        }

        $this->account_root_parameters = $account_root_parameters;
        $this->results = $results;

        return $results;
    }

    /**
     * Report Object Mapper to convert the XML to array of useful PHP objects
     *
     * @param String $xml_string
     * @return Array of gapiReportEntry objects
     */
    protected function reportObjectMapper( $xml_string )
    {
        $xml = simplexml_load_string( $xml_string );

        $this->results = null;
        $results = array( );

        $report_root_parameters = array( );
        $report_aggregate_metrics = array( );

        //Load root parameters

        $report_root_parameters[ 'updated' ] = strval( $xml->updated );
        $report_root_parameters[ 'generator' ] = strval( $xml->generator );
        $report_root_parameters[ 'generatorVersion' ] = strval( $xml->generator->attributes() );

        $open_search_results = $xml->children( 'http://a9.com/-/spec/opensearchrss/1.0/' );

        foreach ( $open_search_results as $key => $open_search_result )
        {
            $report_root_parameters[ $key ] = intval( $open_search_result );
        }

        $google_results = $xml->children( 'http://schemas.google.com/analytics/2009' );

        foreach ( $google_results->dataSource->property as $property_attributes )
        {
            $report_root_parameters[ str_replace( 'ga:', '', $property_attributes->attributes()->name ) ] = strval( $property_attributes->attributes()->value );
        }

        $report_root_parameters[ 'startDate' ] = strval( $google_results->startDate );
        $report_root_parameters[ 'endDate' ] = strval( $google_results->endDate );

        //Load result aggregate metrics

        foreach ( $google_results->aggregates->metric as $aggregate_metric )
        {
            $metric_value = strval( $aggregate_metric->attributes()->value );

            //Check for float, or value with scientific notation
            if ( preg_match( '/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/', $metric_value ) )
            {
                $report_aggregate_metrics[ str_replace( 'ga:', '', $aggregate_metric->attributes()->name ) ] = floatval( $metric_value );
            }
            else
            {
                $report_aggregate_metrics[ str_replace( 'ga:', '', $aggregate_metric->attributes()->name ) ] = intval( $metric_value );
            }
        }

        //Load result entries

        foreach ( $xml->entry as $entry )
        {
            $metrics = array( );
            foreach ( $entry->children( 'http://schemas.google.com/analytics/2009' )->metric as $metric )
            {
                $metric_value = strval( $metric->attributes()->value );

                //Check for float, or value with scientific notation
                if ( preg_match( '/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/', $metric_value ) )
                {
                    $metrics[ str_replace( 'ga:', '', $metric->attributes()->name ) ] = floatval( $metric_value );
                }
                else
                {
                    $metrics[ str_replace( 'ga:', '', $metric->attributes()->name ) ] = intval( $metric_value );
                }
            }

            $dimensions = array( );
            foreach ( $entry->children( 'http://schemas.google.com/analytics/2009' )->dimension as $dimension )
            {
                $dimensions[ str_replace( 'ga:', '', $dimension->attributes()->name ) ] = strval( $dimension->attributes()->value );
            }

            $results[ ] = new gapiReportEntry( $metrics, $dimensions );
        }

        $this->report_root_parameters = $report_root_parameters;
        $this->report_aggregate_metrics = $report_aggregate_metrics;
        $this->results = $results;

        return $results;
    }

    /**
     * Authenticate Google Account with Google
     *
     * @param String $email
     * @param String $password
     */
    function authenticateUser( $email, $password )
    {
        $post_variables = array(
            'accountType' => 'GOOGLE',
            'Email' => $email,
            'Passwd' => $password,
            'source' => Dmf_Gapi::interface_name,
            'service' => 'analytics'
        );

        $response = $this->httpRequest( Dmf_Gapi::client_login_url, null, $post_variables );
        if ( $response[ 'code' ] != '200' )
        {
            return false;
        }

        //Convert newline delimited variables into url format then import to array
        parse_str( str_replace( array( "\n", "\r\n" ), '&', $response[ 'body' ] ), $auth_token );

        if ( substr( $response[ 'code' ], 0, 1 ) != '2' || !is_array( $auth_token ) || empty( $auth_token[ 'Auth' ] ) )
        {
            throw new Exception( 'GAPI: Failed to authenticate user. Error: "' . strip_tags( $response[ 'body' ] ) . '"' );
        }

        $this->auth_token = $auth_token[ 'Auth' ];
        return true;
    }

    /**
     * Generate authentication token header for all requests
     *
     * @return Array
     */
    protected function generateAuthHeader()
    {
        return array( 'Authorization: GoogleLogin auth=' . $this->auth_token );
    }

    /**
     * Perform http request
     * 
     *
     * @param Array $get_variables
     * @param Array $post_variables
     * @param Array $headers
     */
    protected function httpRequest( $url, $get_variables = null, $post_variables = null, $headers = null )
    {
        $interface = Dmf_Gapi::http_interface;

        return $this->curlRequest( $url, $get_variables, $post_variables, $headers );
    }

    /**
     * HTTP request using PHP CURL functions
     * Requires curl library installed and configured for PHP
     * 
     * @param Array $get_variables
     * @param Array $post_variables
     * @param Array $headers
     */
    private function curlRequest( $url, $get_variables = null, $post_variables = null, $headers = null )
    {
        $ch = curl_init();

        if ( is_array( $get_variables ) )
        {
            $get_variables = '?' . str_replace( '&amp;', '&', urldecode( http_build_query( $get_variables ) ) );
        }
        else
        {
            $get_variables = null;
        }

        curl_setopt( $ch, CURLOPT_URL, $url . $get_variables );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); //CURL doesn't like google's cert

        if ( is_array( $post_variables ) )
        {
            curl_setopt( $ch, CURLOPT_POST, true );
            curl_setopt( $ch, CURLOPT_POSTFIELDS, $post_variables );
        }

        if ( is_array( $headers ) )
        {
            curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
        }

        $response = curl_exec( $ch );
        $code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );

        curl_close( $ch );

        return array( 'body' => $response, 'code' => $code );
    }

    /**
     * Case insensitive array_key_exists function, also returns
     * matching key.
     *
     * @param String $key
     * @param Array $search
     * @return String Matching array key
     */
    public static function array_key_exists_nc( $key, $search )
    {
        if ( array_key_exists( $key, $search ) )
        {
            return $key;
        }
        if ( !(is_string( $key ) && is_array( $search )) )
        {
            return false;
        }
        $key = strtolower( $key );
        foreach ( $search as $k => $v )
        {
            if ( strtolower( $k ) == $key )
            {
                return $k;
            }
        }
        return false;
    }

    /**
     * Get Results
     *
     * @return Array
     */
    public function getResults()
    {
        if ( is_array( $this->results ) )
        {
            return $this->results;
        }
        else
        {
            return;
        }
    }

    /**
     * Get an array of the metrics and the matchning
     * aggregate values for the current result
     *
     * @return Array
     */
    public function getMetrics()
    {
        return $this->report_aggregate_metrics;
    }

    /**
     * Call method to find a matching root parameter or 
     * aggregate metric to return
     *
     * @param $name String name of function called
     * @return String
     * @throws Exception if not a valid parameter or aggregate 
     * metric, or not a 'get' function
     */
    public function __call( $name, $parameters )
    {
        if ( !preg_match( '/^get/', $name ) )
        {
            throw new Exception( 'No such function "' . $name . '"' );
        }

        $name = preg_replace( '/^get/', '', $name );

        $parameter_key = Dmf_Gapi::array_key_exists_nc( $name, $this->report_root_parameters );

        if ( $parameter_key )
        {
            return $this->report_root_parameters[ $parameter_key ];
        }

        $aggregate_metric_key = Dmf_Gapi::array_key_exists_nc( $name, $this->report_aggregate_metrics );

        if ( $aggregate_metric_key )
        {
            return $this->report_aggregate_metrics[ $aggregate_metric_key ];
        }

        throw new Exception( 'No valid root parameter or aggregate metric called "' . $name . '"' );
    }

}

/**
 * Class gapiAccountEntry
 * 
 * Storage for individual gapi account entries
 *
 */
class gapiAccountEntry
{

    private $properties = array( );

    public function __construct( $properties )
    {
        $this->properties = $properties;
    }

    /**
     * toString function to return the name of the account
     *
     * @return String
     */
    public function __toString()
    {
        if ( isset( $this->properties[ 'title' ] ) )
        {
            return $this->properties[ 'title' ];
        }
        else
        {
            return;
        }
    }

    /**
     * Get an associative array of the properties
     * and the matching values for the current result
     *
     * @return Array
     */
    public function getProperties()
    {
        return $this->properties;
    }

    /**
     * Call method to find a matching parameter to return
     *
     * @param $name String name of function called
     * @return String
     * @throws Exception if not a valid parameter, or not a 'get' function
     */
    public function __call( $name, $parameters )
    {
        if ( !preg_match( '/^get/', $name ) )
        {
            throw new Exception( 'No such function "' . $name . '"' );
        }

        $name = preg_replace( '/^get/', '', $name );

        $property_key = Dmf_Gapi::array_key_exists_nc( $name, $this->properties );

        if ( $property_key )
        {
            return $this->properties[ $property_key ];
        }

        throw new Exception( 'No valid property called "' . $name . '"' );
    }

}

/**
 * Class gapiReportEntry
 * 
 * Storage for individual gapi report entries
 *
 */
class gapiReportEntry
{

    private $metrics = array( );
    private $dimensions = array( );

    public function __construct( $metrics, $dimesions )
    {
        $this->metrics = $metrics;
        $this->dimensions = $dimesions;
    }

    /**
     * toString function to return the name of the result
     * this is a concatented string of the dimesions chosen
     * 
     * For example:
     * 'Firefox 3.0.10' from browser and browserVersion
     *
     * @return String
     */
    public function __toString()
    {
        if ( is_array( $this->dimensions ) )
        {
            return implode( ' ', $this->dimensions );
        }
        else
        {
            return '';
        }
    }

    /**
     * Get an associative array of the dimesions
     * and the matching values for the current result
     *
     * @return Array
     */
    public function getDimesions()
    {
        return $this->dimensions;
    }

    /**
     * Get an array of the metrics and the matchning
     * values for the current result
     *
     * @return Array
     */
    public function getMetrics()
    {
        return $this->metrics;
    }

    /**
     * Call method to find a matching metric or dimension to return
     *
     * @param $name String name of function called
     * @return String
     * @throws Exception if not a valid metric or dimensions, or not a 'get' function
     */
    public function __call( $name, $parameters )
    {
        if ( !preg_match( '/^get/', $name ) )
        {
            throw new Exception( 'No such function "' . $name . '"' );
        }

        $name = preg_replace( '/^get/', '', $name );

        $metric_key = Dmf_Gapi::array_key_exists_nc( $name, $this->metrics );

        if ( $metric_key )
        {
            return $this->metrics[ $metric_key ];
        }

        $dimension_key = Dmf_Gapi::array_key_exists_nc( $name, $this->dimensions );

        if ( $dimension_key )
        {
            return $this->dimensions[ $dimension_key ];
        }

        throw new Exception( 'No valid metric or dimesion called "' . $name . '"' );
    }

}